Profesor, MAF ITESO
En primer lugar, para poder bajar precios y información sobre opciones de Yahoo, es necesario cargar algunos paquetes de Python. En este caso, el paquete principal será Pandas. También, se usarán el Scipy y el Numpy para las matemáticas necesarias y, el Matplotlib y el Seaborn para hacer gráficos de las series de datos.
In [153]:
#importar los paquetes que se van a usar
import pandas as pd
import pandas_datareader.data as web
import numpy as np
import datetime
from datetime import datetime
import scipy.stats as stats
import scipy as sp
import scipy.optimize as scopt
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
#algunas opciones para Python
pd.set_option('display.notebook_repr_html', True)
pd.set_option('display.max_columns', 6)
pd.set_option('display.max_rows', 10)
pd.set_option('display.width', 78)
pd.set_option('precision', 3)
In [154]:
def def_portafolio(tickers, participacion=None):
if (participacion is None):
part = np.ones(len(tickers))/len(tickers)
portfolio = pd.DataFrame({'Tickers': tickers, 'Participacion': participacion}, index=tickers)
return portfolio
In [155]:
portafolio = def_portafolio(['Acción A', 'Acción B'], [1, 1])
portafolio
Out[155]:
In [156]:
rendimientos = pd.DataFrame({'Acción A': [0.1, 0.24, 0.05, -0.02, 0.2],
'Acción B': [-0.15, -0.2, -0.01, 0.04, -0.15]})
rendimientos
Out[156]:
In [157]:
def valor_portafolio_ponderado(portafolio, rendimientos, name='Valor'):
total_participacion = portafolio.Participacion.sum()
ponderaciones=portafolio.Participacion/total_participacion
rendimientos_ponderados = rendimientos*ponderaciones
return pd.DataFrame({name: rendimientos_ponderados.sum(axis=1)})
In [158]:
rend_portafolio=valor_portafolio_ponderado(portafolio, rendimientos, 'Valor')
rend_portafolio
Out[158]:
In [159]:
total_rend=pd.concat([rendimientos, rend_portafolio], axis=1)
total_rend
Out[159]:
In [160]:
total_rend.std()
Out[160]:
In [161]:
rendimientos.corr()
Out[161]:
In [162]:
total_rend.plot(figsize=(8,6));
In [163]:
def plot_portafolio_rend(rend, title=None):
rend.plot(figsize=(8,6))
plt.xlabel('Año')
plt.ylabel('Rendimientos')
if (title is not None): plt.title(title)
plt.show()
In [164]:
plot_portafolio_rend(total_rend);
Ahora, en forma de función
In [165]:
def get_historical_closes(ticker, start_date, end_date):
p = web.DataReader(ticker, "yahoo", start_date, end_date).sort_index('major_axis')
d = p.to_frame()['Adj Close'].reset_index()
d.rename(columns={'minor': 'Ticker', 'Adj Close': 'Close'}, inplace=True)
pivoted = d.pivot(index='Date', columns='Ticker')
pivoted.columns = pivoted.columns.droplevel(0)
return pivoted
Una vez cargados los paquetes, es necesario definir los tickers de las acciones que se usarán, la fuente de descarga (Yahoo en este caso, pero también se puede desde Google) y las fechas de interés. Con esto, la función DataReader del paquete pandas_datareader bajará los precios solicitados.
Nota: Usualmente, las distribuciones de Python no cuentan, por defecto, con el paquete pandas_datareader. Por lo que será necesario instalarlo aparte. El siguiente comando instala el paquete en Anaconda: conda install -c conda-forge pandas-datareader
In [166]:
closes=get_historical_closes(['AA','AAPL','MSFT','KO'], '2010-01-01', '2016-12-31')
closes
Out[166]:
In [167]:
closes.plot(figsize=(8,6));
Nota: Para descargar datos de la bolsa mexicana de valores (BMV), el ticker debe tener la extensión MX. Por ejemplo: MEXCHEM.MX, LABB.MX, GFINBURO.MX y GFNORTEO.MX.
In [168]:
def calc_daily_returns(closes):
return np.log(closes/closes.shift(1))[1:]
In [169]:
daily_returns=calc_daily_returns(closes)
daily_returns.plot(figsize=(8,6));
In [170]:
daily_returns.corr()
Out[170]:
In [171]:
def calc_annual_returns(daily_returns):
grouped = np.exp(daily_returns.groupby(lambda date: date.year).sum())-1
return grouped
In [172]:
annual_returns = calc_annual_returns(daily_returns)
annual_returns
Out[172]:
In [173]:
def calc_portfolio_var(returns, weights=None):
if (weights is None):
weights = np.ones(returns.columns.size)/returns.columns.size
sigma = np.cov(returns.T,ddof=0)
var = (weights * sigma * weights.T).sum()
return var
In [174]:
calc_portfolio_var(annual_returns)
Out[174]:
In [175]:
def sharpe_ratio(returns, weights = None, risk_free_rate = 0.015):
n = returns.columns.size
if weights is None: weights = np.ones(n)/n
var = calc_portfolio_var(returns, weights)
means = returns.mean()
return (means.dot(weights) - risk_free_rate)/np.sqrt(var)
In [176]:
sharpe_ratio(annual_returns)
Out[176]:
In [177]:
def f(x): return 2+x**2
In [178]:
scopt.fmin(f, 10)
Out[178]:
In [179]:
def negative_sharpe_ratio_n_minus_1_stock(weights,returns,risk_free_rate):
"""
Given n-1 weights, return a negative sharpe ratio
"""
weights2 = sp.append(weights, 1-np.sum(weights))
return -sharpe_ratio(returns, weights2, risk_free_rate)
In [180]:
def optimize_portfolio(returns, risk_free_rate):
w0 = np.ones(returns.columns.size-1, dtype=float) * 1.0 / returns.columns.size
w1 = scopt.fmin(negative_sharpe_ratio_n_minus_1_stock, w0, args=(returns, risk_free_rate))
final_w = sp.append(w1, 1 - np.sum(w1))
final_sharpe = sharpe_ratio(returns, final_w, risk_free_rate)
return (final_w, final_sharpe)
In [181]:
optimize_portfolio(annual_returns, 0.0003)
Out[181]:
In [182]:
def objfun(W, R, target_ret):
stock_mean = np.mean(R,axis=0)
port_mean = np.dot(W,stock_mean)
cov=np.cov(R.T)
port_var = np.dot(np.dot(W,cov),W.T)
penalty = 2000*abs(port_mean-target_ret)
return np.sqrt(port_var) + penalty
In [183]:
def calc_efficient_frontier(returns):
result_means = []
result_stds = []
result_weights = []
means = returns.mean()
min_mean, max_mean = means.min(), means.max()
nstocks = returns.columns.size
for r in np.linspace(min_mean, max_mean, 150):
weights = np.ones(nstocks)/nstocks
bounds = [(0,1) for i in np.arange(nstocks)]
constraints = ({'type': 'eq', 'fun': lambda W: np.sum(W) - 1})
results = scopt.minimize(objfun, weights, (returns, r), method='SLSQP', constraints = constraints, bounds = bounds)
if not results.success: # handle error
raise Exception(result.message)
result_means.append(np.round(r,4)) # 4 decimal places
std_=np.round(np.std(np.sum(returns*results.x,axis=1)),6)
result_stds.append(std_)
result_weights.append(np.round(results.x, 5))
return {'Means': result_means, 'Stds': result_stds, 'Weights': result_weights}
In [184]:
frontier_data = calc_efficient_frontier(annual_returns)
In [185]:
def plot_efficient_frontier(ef_data):
plt.figure(figsize=(12,8))
plt.title('Efficient Frontier')
plt.xlabel('Standard Deviation of the porfolio (Risk))')
plt.ylabel('Return of the portfolio')
plt.plot(ef_data['Stds'], ef_data['Means'], '--');
In [186]:
plot_efficient_frontier(frontier_data)
In [187]:
etf=get_historical_closes(['PICK','IBB','XBI','MLPX','AMLP','VGT','RYE','IEO','AAPL'], '2014-01-01', '2014-12-31')
etf.plot(figsize=(8,6));
In [188]:
daily_returns_etf=calc_daily_returns(etf)
daily_returns_etf
Out[188]:
In [189]:
daily_returns_etf_mean=1000*daily_returns_etf.mean()
daily_returns_etf_mean
Out[189]:
In [190]:
daily_returns_etf_std=daily_returns_etf.std()
daily_returns_etf_std
Out[190]:
In [191]:
daily_returns_ms=pd.concat([daily_returns_etf_mean, daily_returns_etf_std], axis=1)
daily_returns_ms
Out[191]:
In [192]:
from sklearn.cluster import KMeans
In [193]:
random_state = 10
y_pred = KMeans(n_clusters=4, random_state=random_state).fit_predict(daily_returns_ms)
In [194]:
plt.scatter(daily_returns_etf_mean, daily_returns_etf_std, c=y_pred);
plt.axis([-1, 1, 0.01, 0.03]);
In [195]:
import scipy.cluster.hierarchy as hac
In [204]:
daily_returns_etf.corr()
Out[204]:
In [202]:
Z = hac.linkage(daily_returns_etf.corr(), 'single')
In [203]:
# Plot the dendogram
plt.figure(figsize=(25, 10))
plt.title('Hierarchical Clustering Dendrogram')
plt.xlabel('sample index')
plt.ylabel('distance')
hac.dendrogram(
Z,
leaf_rotation=90., # rotates the x axis labels
leaf_font_size=8., # font size for the x axis labels
)
plt.show()
In [ ]: